﻿using System;
using System.Collections.Generic;
using System.Linq;
using Notoric.Commanding.CommandBase;

namespace Notoric.ViewModel
{
    /// <summary>
    /// An undoable command for changing the key of a selected part of a composition.
    /// </summary>
    public class ChangeKeyCommand : UndoableCommandBase
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ChangeKeyCommand"/> class.
        /// </summary>
        /// <param name="undoStack">The undo command stack</param>
        /// <param name="redoStack">The redo command stack</param>
        public ChangeKeyCommand(CommandStack undoStack, CommandStack redoStack)
            : base(undoStack, redoStack)
        {
            AddParameter<ushort>("FromMeasure", ParameterDirection.In);
            AddParameter<double[]>("Key", ParameterDirection.In);
            AddParameter<IList<Elements.PartDescriptor>>("Parts", ParameterDirection.In);
            AddParameter<ushort>("ToMeasure", ParameterDirection.In);

            Name = "Change Key";

            createdKeys = new Dictionary<Elements.PartDescriptor, List<Model.Elements.KeyChangementIndicationElement>>();
            removedKeys = new Dictionary<Elements.PartDescriptor, List<Model.Elements.KeyChangementIndicationElement>>();
            oldKeys = new Dictionary<Elements.PartDescriptor, Model.Elements.KeyIndicationElement>();
        }

        #endregion

        #region Command Logic

        /// <inheritdoc/>
        protected override void CommandOperation(object parameter)
        {
            var composition = (from part in Parts
                               select part.FindOwner<Elements.Composition>()
                              ).Single();
            var modelComposition = (Model.Elements.CompositionElement)composition.AdaptedElement;
            var modelCPart = (Model.Packaging.CompositionPart)modelComposition.PackagePart;

            foreach (var part in Parts)
            {
                var modelPart = (Model.Elements.PartDescriptorElement)part.AdaptedElement;
                var startIndex = 0;
                var lastKC = modelPart.KeyChangements.LastOrDefault(kc => kc.Measure.Number < ToMeasure);
                Model.Elements.KeyIndicationElement oldKey = (lastKC != null ? lastKC.KeyIndication : modelPart.KeyIndication);
                createdKeys[part] = new List<Model.Elements.KeyChangementIndicationElement>();
                removedKeys[part] = new List<Model.Elements.KeyChangementIndicationElement>();

                if (FromMeasure == 1)
                {
                    oldKeys[part] = modelPart.KeyIndication;
                    modelPart.KeyIndication = new Model.Elements.KeyIndicationElement(modelCPart, modelPart, Key);
                }
                else
                {
                    int index = 0;
                    for (; index < modelPart.KeyChangements.Count && modelPart.KeyChangements[index].Measure.Number < FromMeasure; ++index) ;

                    var measure = modelComposition[FromMeasure];
                    var keyChangement = new Model.Elements.KeyChangementIndicationElement(modelCPart, modelPart, measure, Key);

                    createdKeys[part].Add(keyChangement);

                    if (index < modelPart.KeyChangements.Count && modelPart.KeyChangements[index].Measure.Number == FromMeasure)
                        modelPart.KeyChangements[index] = keyChangement;
                    else
                        modelPart.KeyChangements.Insert(index, keyChangement);

                    startIndex = index + 1;
                }

                while (startIndex < modelPart.KeyChangements.Count && modelPart.KeyChangements[startIndex].Measure.Number <= ToMeasure)
                {
                    removedKeys[part].Add(modelPart.KeyChangements[startIndex]);
                    modelPart.KeyChangements.RemoveAt(startIndex);
                }

                if (ToMeasure < modelComposition.MeasuresCount && oldKey != null)
                {
                    if (startIndex >= modelPart.KeyChangements.Count || modelPart.KeyChangements[startIndex].Measure.Number != ToMeasure + 1)
                    {
                        var keyChangement = new Model.Elements.KeyChangementIndicationElement(modelCPart, modelPart, modelComposition[(ushort)(ToMeasure + 1)], oldKey.GetKey());
                        createdKeys[part].Add(keyChangement);
                        modelPart.KeyChangements.Insert(startIndex, keyChangement);
                    }
                }
            }

            var references = modelComposition.GetReferencingPages(modelComposition[FromMeasure], modelComposition[ToMeasure]);
            foreach (var reference in references)
            {
                var interior = (Model.Elements.CWNotation.MusicSectionInterior)reference.Section.Interior;
                foreach (var system in interior.Systems)
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(system);
            }

            var lvInterior = (Model.Elements.CWNotation.MusicSectionInterior)modelCPart.LinearView.MusicSection.Interior;
            Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(lvInterior.Systems[0]);

            composition.FindOwner<Elements.Project>().IsModified = true;
        }

        /// <inheritdoc/>
        protected override void RedoOperation()
        {
            var composition = (from part in Parts
                               select part.FindOwner<Elements.Composition>()
                              ).Single();
            var modelComposition = (Model.Elements.CompositionElement)composition.AdaptedElement;
            var modelCPart = (Model.Packaging.CompositionPart)modelComposition.PackagePart;

            foreach (var part in Parts)
            {
                var modelPart = (Model.Elements.PartDescriptorElement)part.AdaptedElement;
                var startIndex = 0;
                var lastKC = modelPart.KeyChangements.LastOrDefault(kc => kc.Measure.Number < ToMeasure);
                Model.Elements.KeyIndicationElement oldKey = (lastKC != null ? lastKC.KeyIndication : modelPart.KeyIndication);
                createdKeys[part].Clear();

                if (FromMeasure == 1)
                {
                    oldKey = modelPart.KeyIndication;
                    modelPart.KeyIndication = new Model.Elements.KeyIndicationElement(modelCPart, modelPart, Key);
                }
                else
                {
                    int index = 0;
                    for (; index < modelPart.KeyChangements.Count && modelPart.KeyChangements[index].Measure.Number < FromMeasure; ++index) ;

                    var measure = modelComposition[FromMeasure];
                    var keyChangement = new Model.Elements.KeyChangementIndicationElement(modelCPart, modelPart, measure, Key);
                    createdKeys[part].Add(keyChangement);

                    if (index < modelPart.KeyChangements.Count && modelPart.KeyChangements[index].Measure.Number == FromMeasure)
                        modelPart.KeyChangements[index] = keyChangement;
                    else
                        modelPart.KeyChangements.Insert(index, keyChangement);

                    startIndex = index + 1;
                }

                while (startIndex < modelPart.KeyChangements.Count && modelPart.KeyChangements[startIndex].Measure.Number <= ToMeasure)
                    modelPart.KeyChangements.RemoveAt(startIndex);

                if (ToMeasure < modelComposition.MeasuresCount && oldKey != null)
                {
                    if (startIndex >= modelPart.KeyChangements.Count || modelPart.KeyChangements[startIndex].Measure.Number != ToMeasure + 1)
                    {
                        var keyChangement = new Model.Elements.KeyChangementIndicationElement(modelCPart, modelPart, modelComposition[(ushort)(ToMeasure + 1)], oldKey.GetKey());
                        createdKeys[part].Add(keyChangement);
                        modelPart.KeyChangements.Insert(startIndex, keyChangement);
                    }
                }
            }

            var references = modelComposition.GetReferencingPages(modelComposition[FromMeasure], modelComposition[ToMeasure]);
            foreach (var reference in references)
            {
                var interior = (Model.Elements.CWNotation.MusicSectionInterior)reference.Section.Interior;
                foreach (var system in interior.Systems)
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(system);
            }

            var lvInterior = (Model.Elements.CWNotation.MusicSectionInterior)modelCPart.LinearView.MusicSection.Interior;
            Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(lvInterior.Systems[0]);

            composition.FindOwner<Elements.Project>().IsModified = true;
        }

        /// <inheritdoc/>
        protected override void UndoOperation()
        {
            var composition = (from part in Parts
                               select part.FindOwner<Elements.Composition>()
                              ).Single();
            var modelComposition = (Model.Elements.CompositionElement)composition.AdaptedElement;
            var modelCPart = (Model.Packaging.CompositionPart)modelComposition.PackagePart;

            foreach (var part in Parts)
            {
                var modelPart = (Model.Elements.PartDescriptorElement)part.AdaptedElement;

                if (FromMeasure == 1)
                    modelPart.KeyIndication = oldKeys[part];

                foreach (var kc in createdKeys[part])
                    modelPart.KeyChangements.Remove(kc);

                foreach (var kc in removedKeys[part])
                    modelPart.InsertKeyChangement(kc);
            }

            var references = modelComposition.GetReferencingPages(modelComposition[FromMeasure], modelComposition[ToMeasure]);
            foreach (var reference in references)
            {
                var interior = (Model.Elements.CWNotation.MusicSectionInterior)reference.Section.Interior;
                foreach (var system in interior.Systems)
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(system);
            }

            var lvInterior = (Model.Elements.CWNotation.MusicSectionInterior)modelCPart.LinearView.MusicSection.Interior;
            Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(lvInterior.Systems[0]);

            composition.FindOwner<Elements.Project>().IsModified = true;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the first measure at which the key is valid.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The number of the first measure at which the created key is valid.<br/>
        /// This property wraps the "FromMeasure" command parameter.
        /// </para>
        /// </value>
        public ushort FromMeasure
        {
            get { return GetParameterValue<ushort>("FromMeasure"); }
            set { SetParameterValue<ushort>("FromMeasure", value); }
        }

        /// <summary>
        /// Gets or sets the key.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Array"/> of <see cref="Double"/>
        /// </para>
        /// <para>
        /// The new key.
        /// It must be an array of 7 values from the range [-1.0, 1.0].<br/>
        /// This property wraps the "Key" command parameter.
        /// </para>
        /// </value>
        public double[] Key
        {
            get { return GetParameterValue<double[]>("Key"); }
            set { SetParameterValue<double[]>("Key", value); }
        }

        /// <summary>
        /// Gets or sets the modified parts.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="IList{T}"/> of <see cref="Elements.PartDescriptor"/>
        /// </para>
        /// <para>
        /// The collection of parts to which the new key will be set.<br/>
        /// This property wraps the "Parts" command parameter.
        /// </para>
        /// </value>
        public IList<Elements.PartDescriptor> Parts
        {
            get { return GetParameterValue<IList<Elements.PartDescriptor>>("Parts"); }
            set { SetParameterValue<IList<Elements.PartDescriptor>>("Parts", value); }
        }

        /// <summary>
        /// Gets or sets the last measure at which the key is valid.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The number of the last measure at which the created key is valid.<br/>
        /// This property wraps the "FromMeasure" command parameter.
        /// </para>
        /// </value>
        public ushort ToMeasure
        {
            get { return GetParameterValue<ushort>("ToMeasure"); }
            set { SetParameterValue<ushort>("ToMeasure", value); }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the collection of key changements created in individual parts.
        /// </summary>
        private Dictionary<Elements.PartDescriptor, List<Model.Elements.KeyChangementIndicationElement>> createdKeys;
        /// <summary>
        /// Holds the old key indications of individual parts.
        /// </summary>
        private Dictionary<Elements.PartDescriptor, Model.Elements.KeyIndicationElement> oldKeys;
        /// <summary>
        /// Holds the collection of key changements removed from individual parts.
        /// </summary>
        private Dictionary<Elements.PartDescriptor, List<Model.Elements.KeyChangementIndicationElement>> removedKeys;

        #endregion
    }
}
